这是JS 原生方法原理探究系列的第二篇文章。本文会介绍如何实现 Object.create() 方法。关于这个方法的具体用法,MDN 已经描述得很清楚了,这里我们只做简单的介绍,具体的重点在于如何模拟实现。

语法简介

调用:Object.create ( proto , propertiesObject )

返回: 一个新的实例对象

调用这个方法的时候接受两个参数,第一个参数作为返回对象的 __proto__,这个参数只能是 null 或者对象(而且不能是基本类型的包装对象)。

第二个参数作为返回对象的属性描述,它和 Object.defineProperties() 的第二个参数形式是一样的:

{
    propertyA: {
        value: xxx,
        configurable: xxx,
        enumerable: xxx,
        writable: xxx    
    },
    propertyB: {...},
    propertyC: {...}    
}

这个参数的每一个属性都会作为返回对象的属性,而属性值则是相应属性的特性描述(该属性的属性值、是否可读、是否可枚举、是否可配置)。第二个参数只能是对象或者 undefined(表示没有传第二个参数),不能是 null。

ES 规范

对于 Object.create() 的具体实现,规范中其实已经描述得很清楚,可以进入http://es5.github.io/#x15.2.3.5查看:

我简单翻译一下这段话:

create() 方法会创建一个具有指定原型的新对象,当调用该方法的时候,会有如下步骤:

  1. 如果传入的参数 O 不是对象也不是 null,抛出 TypeError 错误
  2. obj 作为调用 new Object() 方法所创建的新对象
  3. obj 的内部属性 [[prototype]] 设置为 O
  4. 如果提供了第二个参数 Properties,且不是 undefined,则调用 Object.defineProperties 方法并传入 objProperties 作为参数,从而为 obj 添加它自己的属性
  5. 返回 obj

可以说,整个过程是一目了然的,我们实现的时候也只需要按照上述步骤实现即可。

代码实现

我们先看第一种实现:

Object.create = function(proto,propertiesObject){
    if(typeof proto != 'object' && proto !== null){
        throw new Error('the first param must be an object or null')
    }
    if(typeof propertiesObject === null){
        throw 'TypeError'
    }
    let obj = {}
    obj.__proto__ = proto
    if(propertiesObject){
        Object.defineProperties(obj,propertiesObject)
    }
    return obj
}

基本上没有什么大问题。不过,我们要留意两个地方:

  • 在这个实现中,没有检测第一个参数是不是基本类型的包装对象,只要传进来的参数是对象,我们就认为是合法的
  • 当传入 null 也即 Object.create(null) 的时候,我们实际上创建了一个很纯粹的空对象,这个对象的原型直接就是 null,Object.prototype 甚至没有出现在该对象的原型链中,这意味这个对象不会继承 Object 的任何方法。

此外,你还可能在其他地方看到类似下面这样的实现:

具体实现如下:

Object.create = function(proto,propertiesObject){
    if(typeof proto != 'object' && proto !== null){
        throw new Error('the first param must be an object or null')
    }
    if(propertiesObject === null){
        throw 'TypeError'
    }
    function F(){}
    F.prototype = proto
    const obj = new F()
    // 处理传参 null 的情况
    if(proto === null){
        obj.__proto__ = proto
    }
    if(propertiesObject){
        Object.defineProperties(obj,propertiesObject)
    }
    return obj
}

这个实现和前面的实现有一个很关键的区别:代码中单独处理了传参 protonull 的情况。可能你会觉得很奇怪:当 protonull 的时候,F.prototype = proto 的效果和 obj.__proto__ = proto 应该是一样的,为什么还要在这种情况下执行一遍 obj.__proto__ = proto 呢?这似乎说明,用 null 重写 F 的原型后,新创建的实例的 __proto__ 并不是 null —— 事实上确实不是。

关于调用构造函数时会执行的操作,规范明确提到了这一点:

If Type(proto) is not Object, set the [[Prototype]] internal property of obj to the standard built-in Object prototype object as described in 15.2.4.

由于我们这里是通过 new 构造函数的方式创建新对象(而不是像之前那样通过对象字面量的形式),所以在 new F 的时候,内部会检测 F 的原型是不是对象,如果不是对象,那么会把实例的 __proto__ 链接到内建的 Object.prototype。因此,这里新创建的实例的 __proto__ 还真不是 null。

但根据 Object.create 的实现规范,这里必须让实例的 __proto__ 指向 null,所以才需要执行 obj.__proto__ = proto 去手动设置对象原型。

当然,如果我们像第一个实现那样,直接去设置对象的 __proto__,而不是采用构造函数的方式,就不存在这个问题了。